﻿<!DOCTYPE html>

<html lang="en" xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta charset="utf-8" />
<title>WebGL Lesson 6 - wysaid</title>
<script src="webgl_matrix.js" charset="utf-8"></script>
<script src="demo-lesson6.js" charset="utf-8"></script>
</head>
<body style="font-family:微软雅黑;" onresize="bodyResize()" onload="bodyResize()">
<h1>第六话，了解OpenGL坐标系，实现并封装部分矩阵运算，通过矩阵运算实现一个2D线条模拟的3D场景</h1>
<p>时隔数月，终于把该了解的了解了很多。于是厚颜继续更新。<br>废话不多说，由于本教程将不引入任何第三方库，所有demo的代码均使用WebGL&Javascript代码完整提供，<br>而WebGL 并不提供矩阵相关的运算。 一旦涉及到3D，必然要先讲讲矩阵部分了。</p>
<p>首先是OpenGL的坐标系，我们须明确，<br><font color="green">OpenGL使用右手笛卡尔坐标系，从左到右x递增，从下到上y递增，从远到近z递增</font><br>而通常我们的屏幕原点是在左上角，就xOy平面来说，y方向是反的。<br>初始化状态下，当前viewport指定的窗口(画布)范围恰好为-1到1。也就是前面几章所有demo用到的范围（前面几章仅仅进行了平面处理，每次都重绘整张画布）</p>
<p>由于本教程仅针对WebGL，所以仅粗略的介绍相关运算的知识。看完依旧不清楚的请自行查阅相关资料。<font color="red">须知：本章知识并不需要完全掌握，只要会使用就行。</font>本章会封装出一个webgl_matrix.js完成所有的矩阵运算，请不要完全跳过本章内容。</p>
<h3>OpenGL 坐标系&坐标变换等的一些概念</h3>
<h5>世界坐标系</h5>
<p>世界坐标系是在OpenGL中描述场景的坐标系。比如我们的三维世界中有一条线段AB，顶点A坐标为(Xa, Ya, Za)， 顶点B坐标为(Xb, Yb, Zb)，这里的A和B就属于世界坐标系</p>

<h5>模型坐标系 &模型变换</h5>
<p>模型坐标系是描述一个模型的坐标系。就比如上面提到的线段AB所构成的线段模型，如果它的长度为1， 那么A可以为(1, 0, 0), B为(0, 0, 0)。当然，我们可以有无数种A和B的组合让这条线段长度为1。 而把这里的A和B进行移位、旋转和缩放之后放到世界坐标系里面的过程叫做模型变换。较典型的OpenGL函数有glRotate, glTransform, glScale等</p>

<h5>法向量变换 </h5>
<p>法向量用来计算光照，光照方向和法向量的夹角影响光的强度， 法向量也决定了物体是面向光还是背光。相关资料参考：（<a href="http://www.songho.ca/opengl/gl_normaltransform.html" target="_blank">http://www.songho.ca/opengl/gl_normaltransform.html</a>）</p>

<h5>视点坐标系 &视点变换</h5>
<p>在OpenGL中没有单独的view matrix。而 GL_MODELVIEW 矩阵则是 Model Matrix(模型矩阵) 与 View Matrix (视觉矩阵) 的组合 (ViewMatrix * ModelMatrix )。较典型的视点变换函数为gluLookAt。 而由于 ViewMatrix为 左乘数， 所以我们一般对GL_MODELVIEW 进行 loadIdentity之后先进行gluLookAt。当然这些东西WebGL都是没有的，稍后我们将自己计算出相关矩阵。</p>

<h5>裁剪坐标系 &投影变换</h5>
<p>视点坐标系与投影矩阵相乘得到裁剪坐标系。投影矩阵定义了一个平截头体，将平截头体以内的场景作为绘制对象，超出部分裁剪掉。投影变换分为透视投影(perspective)和平行投影(ortho)。</p>

<h5>规格化设备坐标系 </h5>
<p>将裁剪坐标系除以w所得(透视除法)。这一步在透视投影过程中称为透视除法（Perspective Division），这是透视投影变换的第2步，经过这一步，就丢弃了原始的z值（得到了规则观察体中对应的z值，篇幅所限，请自行查阅资料），顶点才算完成了投影。此步过后，x, y, z三个方向的坐标将被标准化(取值范围为[-1, 1]。</p>

<h5>屏幕坐标系</h5>
<p>将规格化设备坐标系通过viewport变换就可以得到屏幕坐标系。屏幕坐标系当然是仅包含x和y的，z值将作为深度信息确定一些遮挡问题（如果启用GL_DEPTH_TEST的话）。简单概括来就是，假如viewport为 x0, y0, w, h， 那么我们只需要下面的操作：<br>
x = x0 + (x + 1) * w / 2;<br>
y = y0 + (y + 1) * h / 2;<br>
z = (z + 1) / 2;<br></p>

<p><font color="green">讲得有点多，如果还是不大明白的话，不妨再看看这个： <a href="http://www.songho.ca/opengl/gl_transform.html" target="_blank">http://www.songho.ca/opengl/gl_transform.html</a></font>。 这里将一些必要的算式以图片形式给了处理，非常便于理解。看不明白也没关系，继续往下看，知道怎么用的就好</p>

<h3>好的，相关资料就介绍这么多，下面开始动手吧！</h3>
<p>凡事讲究循序渐进，为了让本章内容更加深刻，<font color="red">本章demo将不使用WebGL，而使用HTML5方式，以普通2D线条模拟出3D模型的效果。</font>（当然了， 须得简单结合上面的矩阵变换的知识，实现的效果也较为简单）</p>
<p>首先当然需要先创建一个图形界面了（个人喜好），样式如下：</p>
<div style="position:relative;border:groove;background-color: #ddd;margin:20px;width:80%;height:350px;"><div style="margin:10px 0;position:relative;width:100%;height:300px;background-color: #d00;">
<canvas style="width:100%;height:100%;position:relative;"></canvas></div>
<input style="width:20%;height:30px;margin: 0 3%;" type="button" value="reset" title="重置画面"><input style="width:20%;height:30px;margin: 0 3%;" type="button" value="ortho" title="平行投影"><input style="width:20%;height:30px;margin: 0 3%;" type="button" value="perspective" title="透视投影">
</div>

<h5>那么这个demo需要些什么矩阵功能呢？</h5>
<p>本demo将包含一个随机生成的较复杂的较大的模型和一个较简单的小立方体。由于教程窗口大小有限，大的模型不需要随便动，但是可以使用鼠标旋转。较小的立方体则不停地游荡和旋转。</p>
<p>所以我们需要的至少包含平行&透视投影矩阵算法，模型旋转矩阵算法。（其中模型移动的算法可有可无，因为我们使用HTML5方式模拟，可以把线条画到指定的坐标上。</p>
<p>最后我们还需要一个映射算法，将最终结果映射到屏幕上</p>
<p><font color = "red">其中矩阵算法部分较为复杂，并且不是必须掌握，所以不详细讲述。</font>有兴趣的读者可以参阅，本章我编写了一个js文件，<a href="webgl_matrix.js" target="_blank">点击此处查看</a></p>
<p>这里简单讲述一下我编写的<font color="green">webgl_matrix.js</font>里面的一些转换方法</p>
<p>
在webgl_matrix.js里面包含三个我们需要用到的类，分别是<font color="red"><b>三维向量WYVec3，四维向量WYVec4以及4x4矩阵WYMat4</b></font>。
使用时可以直接new 一个这类对象出来使用。提供了本次demo需要用到（以及部分现在用不到，以后可能会用到）的方法。<br>
当然，还有很多静态方法
比如 <br><b>WYMat4.makeLookAt</b> ，顾名思义，参数跟gluLookAt是一样的，生成一个该状态下的4x4矩阵。<br>
<b>WYMat4.makeOrtho</b>, 参数跟glOrtho一样，生成一个该状态下的平行投影矩阵。
<b>WYMat4.makePerspective</b>, 参数跟gluPerspective一样，生成一个该状态下的透视投影矩阵。
还有一些比如旋转，平移之类的，就不多赘述了。
</p>

<h4>下面来详细讲一下怎么做吧。</h4>

<p>首先当然是生成简单模型了。本次demo的模型超级简单，就是30~50个3维的点，然后我们把它们两两使用直线相连，就可以得到一个看起来还算比较立体的效果(30*29 = 870)。于是乎，我们的代码可能就是下面这样的：（部分代码，不完整。完整代码请参见末尾demo给出）
</p>
<pre>
//生成大模型:
var bigModel = null;
function genBigModel(width, height, depth)
{
	bigModel = new Array(30);
	for(var i = 0; i != 30; ++i)
	{
		bigModel = new WYVec4(Math.random() * width * 2 - width,
			Math.random() * height * 2 - height,
			Math.random() * depth * 2 - depth,
			1.0);
	}
}

//生成小模型：
var smallModel = null;
function genSmallModel(size)
{
	smallModel = new Array(
		new WYVec4(-size, -size, size, 1.0),
		new WYVec4(size, -size, size, 1.0),
		new WYVec4(size, size, size, 1.0),
		new WYVec4(-size, size, size, 1.0),
		new WYVec4(-size, -size, -size, 1.0),
		new WYVec4(size, -size, -size, 1.0),
		new WYVec4(size, size, -size, 1.0),
		new WYVec4(-size, size, -size, 1.0));
}
</pre>

<p>接下来就要用到我们封装好的矩阵和向量算法了， 代码如下</p>

<pre>
//模型转换矩阵：（初始化为单位矩阵）
var modelViewMatrix = WYMat4.makeIdentity();
var smallModelViewMatrix = WYMat4.makeIdentity(); //专给小模型使用

//投影矩阵：(初始化为大模型xOy平面下尺寸大小的平行投影)
var perspectiveMatrix = WYMat4.makeIdentity();
var orthoMatrix = WYMat4.makeIdentity();
var usePerspective = true;  //设置Flag标记是否使用透视投影。
var projMatrix = perspectiveMatrix;


//viewport大小也初始化为大模型xOy平面下尺寸大小
var viewport = new WYVec4(0.0, 0.0, modelXMax, modelYMax);

//设置flag，标记当前使用的是什么方式的投影。
var usePerspective = false;

//当画布大小改变时，我们需要做的事：
function resizeCanvas(w, h)
{
	viewport.data[2] = w;
	viewport.data[3] = h;
	if(usePerspective)
		projMatrix = WYMat4.makePerspective(45.0, w / h, -1.0, 1.0);
	else
		projMatrix = WYMat4.makeOrtho(0.0, w, 0.0, h, -1.0, 1.0)
	//使用默认的视点就够了，所以不乘以LookAt生成的矩阵.
}

//定义一系列需要用到的变量（大模型，大模型默认参数，小模型，小模型参数等）
var bigModel = null;
var smallModel = null;
var modelXMax = 250.0;
var modelYMax = 250.0;
var modelZMax = 250.0;
var smallModeSize = 30.0;

//小模型运动参数
var cx = 0;
var cy = 0;
var dx = Math.random() + 1;
var dy = Math.random() + 1;


// 接下来就是初始化矩阵函数，以及三个设置函数
function resizeCanvas(w, h)
{
	viewport.data[2] = w;
	viewport.data[3] = h;
	var len = (w < h ? w : h);

	orthoMatrix = WYMat4.makeOrtho(-w / 2.0, w / 2.0, -h / 2.0, h / 2.0, -1.0, 1.0);
	perspectiveMatrix = WYMat4.makePerspective(45.0, w / h, -1.0, 1.0);
	modelViewMatrix = WYMat4.makeLookAt(0.0, 0.0, len, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0);
	smallModelViewMatrix = WYMat4.makeTranslation(-w / 2.0, -h / 2.0, 0.0);
	if(usePerspective)
	{
		projMatrix = perspectiveMatrix;
	}
	else
	{		
		projMatrix = orthoMatrix;
	}	
	cx = 0;
	cy = 0;
}

//设置为平行投影
function setOrtho()
{
	usePerspective = false;
	projMatrix = orthoMatrix;
}

//设置为透视投影
function setPerspective()
{
	usePerspective = true;
	projMatrix = perspectiveMatrix;
}

//重置模型大小
function resetModel()
{
	var len = (viewport.data[2] > viewport.data[3] ? viewport.data[3] : viewport.data[2]) / 2.0;
	genBigModel(len, len, len);
	genSmallModel(len / 20.0 + Math.random() * (len / 20.0));
}

//接下来就是绘制函数。如下
function drawBigModel(ctx)
{
	var modelPoint = new Array(bigModel.length);

	for(var i = 0; i < bigModel.length; ++i)
	{
		modelPoint[i] = new WYVec3();
		WYMat4.projectM4(bigModel[i], modelViewMatrix, projMatrix, viewport, modelPoint[i]);
	}

	for(var i = 0; i < bigModel.length; ++i)
	{
		for(var j = i+1; j < bigModel.length; ++j)
		{
			ctx.moveTo(modelPoint[i].data[0], modelPoint[i].data[1]);
			ctx.lineTo(modelPoint[j].data[0], modelPoint[j].data[1]);
		}
	}
	if(autoRotate)
	{
		modelViewMatrix = WYMat4.mat4Mul(modelViewMatrix, WYMat4.makeXRotation(0.01));
		modelViewMatrix = WYMat4.mat4Mul(modelViewMatrix, WYMat4.makeYRotation(0.01));
		modelViewMatrix = WYMat4.mat4Mul(modelViewMatrix, WYMat4.makeZRotation(Math.random() / 100.0));
	}
}

function drawSmallModel(ctx)
{
	var modelPoint = new Array(smallModel.length);
	for(var i = 0; i < smallModel.length; ++i)
	{
		modelPoint[i] = new WYVec3();
		WYMat4.projectM4(smallModel[i], smallModelViewMatrix, orthoMatrix, viewport, modelPoint[i]);
	}

	for(var i = 0; i < smallModel.length; ++i)
	{
		for(var j = i+1; j < smallModel.length; ++j)
		{
			ctx.moveTo(modelPoint[i].data[0] + cx, modelPoint[i].data[1] + cy);
			ctx.lineTo(modelPoint[j].data[0] + cx, modelPoint[j].data[1] + cy);
		}
	}
	ctx.fillText("这不是WebGL哟", modelPoint[0].data[0] + cx, modelPoint[0].data[1] + cy);
	cx += dx;
	cy += dy;
	if(cx < 0 || cx > viewport.data[2])
		dx = -dx;
	if(cy < 0 || cy > viewport.data[3])
		dy = -dy;
	smallModelViewMatrix = WYMat4.mat4Mul(smallModelViewMatrix, WYMat4.makeXRotation(Math.random() / 10.0));
	smallModelViewMatrix = WYMat4.mat4Mul(smallModelViewMatrix, WYMat4.makeYRotation(Math.random() / 20.0));
	smallModelViewMatrix = WYMat4.mat4Mul(smallModelViewMatrix, WYMat4.makeZRotation(Math.random() / 30.0));	
}

function drawCanvas(cvsName)
{
	var cvsObj = document.getElementById(cvsName);
	var cvsContext = cvsObj.getContext("2d");
	cvsContext.fillStyle="#000";
	cvsContext.clearRect(0, 0, cvsObj.width, cvsObj.height);
	cvsContext.fillText("鼠标点击红色区域可以旋转大的模型", 20, 20);
	cvsContext.lineWidth = 2;
	cvsContext.beginPath();
	cvsContext.strokeStyle = "#ff0";
	drawBigModel(cvsContext);
	cvsContext.stroke();
	cvsContext.closePath();
	cvsContext.beginPath();
	cvsContext.strokeStyle = "#00f";
	drawSmallModel(cvsContext);
	cvsContext.stroke();
	cvsContext.closePath();
}

</pre>

<p>至此，我们的工作大致完成。当然，有一部分界面控制代码就不详细说明了。我们的demo效果如下（鼠标拖拽红色部分可以旋转模型）</p>
<p>也许你觉得并不稀奇，但是本demo没有用到WebGL的API，所以，是纯2D线条画出来的哟。</p>

<div style="position:relative;border:groove;background-color: #ddd;margin:20px;width:80%;height:550px;"><div style="margin:10px 0;position:relative;width:100%;height:500px;background-color: #d00;" id="canvas_father" onmousedown="mouseDown(event)" onmouseup="mouseUp()" onmousemove="rotate(event)">
<canvas id="webgl-lesson6" style="width:100%;height:100%;position:relative;"></canvas></div>
<input style="width:20%;height:30px;margin: 0 3%;" type="button" value="genModel" title="重置画面" onclick="resetModel()"><input style="width:20%;height:30px;margin: 0 3%;" type="button" value="ortho" title="平行投影" onclick = "setOrtho()"><input style="width:20%;height:30px;margin: 0 3%;" type="button" value="perspective" title="透视投影" onclick="setPerspective()"></div>

<p>完整的demo点击下面的按钮打开。请自行右键另存代码并查看。<br>第六期教程到此为止，请关注lesson 6。系列教程地址:webgl-lesson.wysaid.org</p>

<h2>附：</h2>
<p><b><a href="https://raw.github.com/wysaid/WebGL-Lessons/gh-pages/Lesson6/webgl_matrix.js" target="_blank">webgl_matrix.js</a></b></p>
<p><b><a href="https://raw.github.com/wysaid/WebGL-Lessons/gh-pages/Lesson6/demo-lesson6.js" target="_blank">demo-lesson6.js</a></b></p>
<p><b><a href="https://raw.github.com/wysaid/WebGL-Lessons/gh-pages/Lesson6/demo_lesson6.html" target="_blank">demo_lesson6.html</a></b></p>



<script>
function bodyResize()
{
	var divObj = document.getElementById("canvas_father");
	var cvsObj = document.getElementById("webgl-lesson6");
	cvsObj.width = divObj.clientWidth;
	cvsObj.height = divObj.clientHeight;
	resizeCanvas(cvsObj.width, cvsObj.height);
	drawCanvas('webgl-lesson6');
}

setInterval("drawCanvas('webgl-lesson6')", 20);

</script>
</body>
</html>